/*
 * $Id: ssh2.cpp 9ce65bed8ef4 2012/08/26 02:14:51 pgurumur $
 * Copyright (c) 2012 Prabhu Gurumurthy <pgurumur@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *

 * __version__  = "$Revision: 9ce65bed8ef4 $"
 * __author__   = "$Author: pgurumur $"
 * __modified__ = "$Date: 2012-08-26 02:14:51Z $"
 */
#include <iostream>
#include <sstream>
#include <string>

#include "consts.h"
#include "log.h"
#include "channel.h"
#include "ssh2.h"

const char *DefMesg = "Normal shutdown";
const char *DefBanner = "LIBSSH_2.0";
const char *DefServiceName = "ssh";
static void kbd_callback(const char *, int, const char *, int, int,
      const LIBSSH2_USERAUTH_KBDINT_PROMPT *,
      LIBSSH2_USERAUTH_KBDINT_RESPONSE *, void **);

ssh2::ssh2(std::string &hostname, std::string &username, const u_int16_t port) :
   _host(hostname), _user(username), _port(port)
{
   if ((_port < MinPort) || (_port > MaxPort)) {
      err(1, "invalid port!");
   }

   if (_port == 0)
      _port = 22;

   if (_host.length() <= 0) {
      err(1, "host has invalid length!");
   }

   if (_user.length() <= 0) {
      _user = getenv("USER");
   }

   assert(_user.length());
}

ssh2::ssh2(const ssh2 &Client)
{
   *this = Client;
}

ssh2 &
ssh2::operator= (const ssh2 &Client)
{
   _host = Client._host;
   _port = Client._port;
   _passkey = Client._passkey;
   _privkey = Client._privkey;
   _pubkey = Client._pubkey;
   _user = Client._user;
   _sock = Client._sock;
   _af = Client._af;
   _session = Client._session;

   return *this;
}

ssh2::~ssh2()
{
   if (_session) {
      libssh2_session_disconnect(_session, DefMesg);
      libssh2_session_free(_session);
   }

   if (_sock) {
      close(_sock);
   }
}

void
ssh2::Connect(int family)
{
   struct addrinfo hints, *results, *res;

   memset(&hints, 0, sizeof(hints));
   hints.ai_family = PF_UNSPEC;
   if (family) {
      hints.ai_family = family;
   }

   hints.ai_socktype = SOCK_STREAM;
   int addrerr = getaddrinfo(_host.c_str(), DefServiceName, &hints, &res);
   if (addrerr) {
      err(1, "%s: getaddrinfo(): %s", __func__, gai_strerror(addrerr));
   }

   for (results = res; res; res = res->ai_next) {
      _sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
      if (_sock < 0) {
         freeaddrinfo(res);
         err(1, "%s: socket()", __func__);
      }
      else {
         int setopt = 0;
         const u_int32_t recvbuf = MinBuf;
         setopt = setsockopt(_sock, SOL_SOCKET, SO_RCVBUF, &recvbuf,
               sizeof(u_int32_t));
         if (setopt < 0) {
            err(1, "setting recv buffer: %s setsockopt()", __func__);
         }

         /*
         int block = fcntl(_sock, F_GETFL);
         if (block < 0)
            err(1, "fcntl(F_GETFL)");

         int value = block | O_NONBLOCK;
         if (fcntl(_sock, F_SETFL, value) < 0)
            err(1, "fcntl(F_SETFL)");

         */

         if (connect(_sock, res->ai_addr, res->ai_addrlen) < 0)
            if (errno != EINPROGRESS) {
               freeaddrinfo(res);
               err(1, "%s: connect()", __func__);
            }

         /*
         block &= ~O_NONBLOCK;
         if (fcntl(_sock, F_SETFL, block) < 0)
            err(1, "fcntl(F_SETFL)");

         */

         break;
      }
   }

   assert(_sock);

   if (res) {
      freeaddrinfo(res);
   }

   _session = libssh2_session_init();
   _version = libssh2_version(LIBSSH2_VERSION_NUM);

   int rc = libssh2_session_startup(_session, _sock);
   if (rc == LIBSSH2_ERROR_EAGAIN) {
      err(1, "libssh2_session_startup() %s", _geterror().c_str());
   }

   if (libssh2_banner_set(_session, DefBanner)) {
      err(1, "libssh2_banner_set()");
   }

   int type = 0;
   size_t length = 0;
   // const char *fingerprint = libssh2_hostkey_hash(_session, &length, &type);
   const char *fingerprint = libssh2_session_hostkey(_session, &length, &type);

   LIBSSH2_KNOWNHOSTS *nh = libssh2_knownhost_init(_session);
   if (nh && fingerprint) {
      std::stringstream oss;
      oss << getenv("HOME") << "/.ssh/known_hosts";
      std::string knownhosts = oss.str();
      libssh2_knownhost_readfile(nh, knownhosts.c_str(),
            LIBSSH2_KNOWNHOST_FILE_OPENSSH);

      struct libssh2_knownhost *host;
      int check = libssh2_knownhost_checkp(nh, GetHost().c_str(), 22,
            fingerprint, length,
            LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW,
            &host);
      std::cout << "Host check: " << check << " key: " <<
         ((check <= LIBSSH2_KNOWNHOST_CHECK_MISMATCH) ? host->key : "<none>")
         << std::endl;

      libssh2_knownhost_free(nh);
   }

   char message[MaxLine];
   memset(message, '\0', MaxLine);
   for (int ix = 0; ix < MaxFingerPrint; ix++) {
      snprintf(&message[ix], sizeof(ix) + 1, "%02X ",
            (unsigned char)fingerprint[ix]);
   }

   std::cout << message << std::endl;
}

int
ssh2::Authenticate(std::string password)
{
   bool retval = false;

   std::cout << password.length() << std::endl;
   if (password.length()) {
      retval = _passauth();
   }
   else {
      std::string authlist = libssh2_userauth_list(
            _session, _user.c_str(), _user.length());

      if (_pkauth(authlist)) {
         retval = true;
      }
      else if (_passauth(authlist)) {
         retval = true;
      }
      else if (_kiauth(authlist)) {
         retval = true;
      }
   }

   return retval;
}

int
ssh2::_pkauth(std::string &list)
{
   bool retval = false;
   if (strstr(list.c_str(), "publickey")) {
      std::stringstream oss;
      oss << getenv("HOME") << "/.ssh/id_rsa";
      _privkey = oss.str();

      oss.str("");
      oss << getenv("HOME") << "/.ssh/id_rsa.pub";
      _pubkey = oss.str();

      oss.str("");
      oss << "Enter passpharse for " << _privkey << ": ";
      _passkey = getpass(oss.str().c_str());
      int rc  = libssh2_userauth_publickey_fromfile(_session, _user.c_str(),
               _pubkey.c_str(), _privkey.c_str(), _passkey.c_str());
      if (rc == 0)
         retval = true;
   }

   return retval;
}

int
ssh2::_passauth(std::string list)
{
   bool retval = false;
   bool flag = false;
   if (list.length()) {
      if (strstr(list.c_str(), "password")) {
         flag = true;
      }
   }
   else {
      flag = true;
   }

   if (flag) {
      std::stringstream oss;
      oss << "Enter password for " << _user << " on " << _host << ": ";
      std::string password = getpass(oss.str().c_str());
      int rc = libssh2_userauth_password(_session, _user.c_str(),
            password.c_str());
      if (rc == 0)
         retval = true;
   }

   return retval;
}

int
ssh2::_kiauth(std::string &list)
{
   bool retval = 0;
   if (strstr(list.c_str(), "keyboard-interactive")) {
      int rc = libssh2_userauth_keyboard_interactive(_session, _user.c_str(),
            &kbd_callback);
      if (rc == 0)
         retval = true;
   }

   return retval;
}

static void 
kbd_callback(const char *name, int name_len,
      const char *instruction, int instruction_len,
      int num_prompts, const LIBSSH2_USERAUTH_KBDINT_PROMPT *prompts,
      LIBSSH2_USERAUTH_KBDINT_RESPONSE *responses, void **abstract)
{       
   (void)name;
   (void)name_len;
   (void)instruction;
   (void)instruction_len;
                           
   std::string password = "Enter password: ";
   if (num_prompts == 1) {
      responses[0].text = strdup(password.c_str());
      responses[0].length = password.length();
   }
   
   (void)prompts;
   (void)abstract;
}       

LIBSSH2_SESSION *
ssh2::GetSession() const
{
   return _session;
}

std::string
ssh2::GetUser() const
{
   return _user;
}

std::string
ssh2::GetHost() const
{
   return _host;
}

void
ssh2::GetChannel(std::string file, bool sendstdout)
{
   int block = fcntl(_sock, F_GETFL);
   if (block < 0)
      err(1, "fcntl(F_GETFL)");

   block &= ~O_NONBLOCK;
   if (fcntl(_sock, F_SETFL, block) < 0)
      err(1, "fcntl(F_SETFL)");

   channel sshc;
   if (file.length()) {
      std::cout << "Not implemented!" << std::endl;
   }
   else {
      sshc.open(_session);
      sshc.shell();
   }

   sendstdout = sendstdout;
}

std::string
ssh2::_geterror()
{
   int stringlen = 0;
   char errmesg[MaxLine];
   memset(errmesg, '\0', MaxLine);

   (void)libssh2_session_last_error(_session, (char **)&errmesg, &stringlen, 0);
   std::string mesg = errmesg;

   return mesg;
}
